/**
 * ByteBuffer.js
 * 1，字节流基本类型封装
 * 2，long,int64类型实现
 * 3，utf8编码实现
 * 
 *  字节流打包:
 *  var buffer=new ByteBuffer(128);
 *  buffer.writeString("test string");
 *  buffer.writeHex(0x04);
 *  buffer.writeChar("|");
 *  buffer.writeUint32(1000);
    buffer.writeInt64(-14725836936900);
    socket.send(buffer.buffer,buffer.offset);
    字节流解包:
    var len=socket.read(data);
    
    var buffer=new ByteBuffer(len);
    buffer.setBufferData(data);
 *  var str=buffer.readString();
 *  var int32=buffer.readInt32();
 *  ....
 */

export var ByteBuffer = function(length) {
    this.buffer = new ArrayBuffer(length);
    // this.byteStream=new Uint8Array(this.buffer);
    this.dataView = new DataView(this.buffer);
    // 缓冲区偏移量
    this.offset = 0;

    this.setBufferData = function(buffer) {
        this.buffer = buffer;
        this.dataView = new DataView(this.buffer);
        this.offset = 0;
        return this.dataView;
    }

    // copy buffer to another
    // buffer,m.appendArrayBuffer(m.offset+2,n.buffer,0,5);
    this.appendArrayBuffer = function(toPos, fromBuffer, fromPos, length) {
        // console.log("appendArrayBuffer toPos=%d,fromPos=%d,length=%d
        // \n",toPos,fromPos,length);

        var uarr = new Uint8Array(fromBuffer);
        for (var i = fromPos; i < length; i++) {
            this.dataView.setInt8(toPos++, uarr[i], true);
        }
    }

    // 将uarr复制到当前缓冲区
    this.appendUint8Array = function(toPos, uarr) {
        for (var i = 0; i < uarr.length; i++) {
            this.dataView.setInt8(toPos++, uarr[i], true);
        }
    }

    this.initHeader = function() {
        this.offset += 9;
    }

    this.setHeader = function(input) {
        this.dataView.setInt8(0, 0x7c, true);
        this.dataView.setUint32(1, input, true);
        this.dataView.setUint32(5, input - 5, true);
    }

    // writeChar("a")
    this.writeChar = function(input) {
        this.dataView.setInt8(this.offset++, input.charCodeAt(), true);
        return this.offset;
    }

    // writeHex(0x04);
    this.writeHex = function(input) {
        this.dataView.setInt8(this.offset++, input);
        return this.offset;
    }

    this.writeUint16 = function(input) {
        this.dataView.setUint16(this.offset, input, true);
        this.offset += 2;
        return this.offset;
    }

    this.writeUint32 = function(input) {
        this.dataView.setUint32(this.offset, input, true);
        this.offset += 4;
    }

    this.writeInt32 = function(input) {
        this.dataView.setInt32(this.offset, input, true);
        this.offset += 4;
    }

    this.writeInt64 = function(input) {
        var sign = input < 0;
        if (sign)
            input = -1 - input;
        for (var i = 0; i < 8; i++) {
            var mod = input % 0x100;
            input = (input - mod) / 0x100;
            var v = sign ? mod ^ 0xFF : mod;
            this.dataView.setUint8(this.offset++, v);
        }
        return this.offset;
    }

    this.readInt64 = function() {
        var bytes = new Uint8Array(this.buffer, this.offset, 8);
        var sign = bytes[7] >> 7;
        this.offset += 8;
        var sum = 0;
        var digits = 1;
        for (var i = 0; i < 8; i++) {
            var value = bytes[i];
            sum += (sign ? value ^ 0xFF : value) * digits;
            digits *= 0x100;
        }
        return sign ? -1 - sum : sum;
    }

    this.writeString = function(input) {
        var utf8Length = this.UTF8Length(input);
        this.offset = this.writeUint16(utf8Length);
        this.stringToUTF8(input);

        return this.offset;
    }

    // ======================

    this.readUint32 = function() {
        var ret = this.dataView.getUint32(this.offset, true);
        this.offset += 4;
        return ret;
    }

    this.readInt32 = function() {
        var ret = this.dataView.getInt32(this.offset, true);
        this.offset += 4;
        return ret;
    }

    this.readInt16 = function() {
        var ret = this.dataView.getInt16(this.offset, true);
        this.offset += 2;
        return ret;
    }

    this.readUint16 = function() {
        var ret = this.dataView.getUint16(this.offset, true);
        this.offset += 2;
        return ret;
    }

    this.readUint8 = function() {
        var ret = this.dataView.getUint8(this.offset);
        this.offset += 1;
        return ret;
    }

    this.readString = function() {
        var length = this.readInt16();
        var uint8Array = new Uint8Array(this.buffer, this.offset, length);
        var ret = this.parseUTF8(uint8Array, 0, uint8Array.length);
        this.offset += uint8Array.length;
        return ret;
    }

    this.readFloat32 = function() {
        var ret = this.dataView.getFloat32(this.offset, true);
        this.offset += 4;
        return ret;
    }

    this.readFloat64 = function() {
        var ret = this.dataView.getFloat64(this.offset, true);
        this.offset += 4;
        return ret;
    }

    this.readList = function(types) {
        if (types.length <= 0)
            return;
        var list = [];
        var length = this.readInt16();
        for (var row = 0; row < length; row++) {
            var values = {};
            for (var i = 0; i < types.length; i++) {
                var type = types[i];

                if (types[i] == "int") {
                    values[type] = this.readInt32();
                } else if (types[i] == "short") {
                    values[type] = this.readInt16();
                } else if (types[i] == "float") {
                    values[type] = this.readFloat32();
                } else if (types[i] == "double") {
                    values[type] = this.readFloat64();
                } else if (types[i] == "string") {
                    values[type] = this.readString();
                }
            }
            list[row] = values;
        }
        return list;
    }

    this.stringToUTF8 = function(input) {
        for (var i = 0; i < input.length; i++) {
            var charCode = input.charCodeAt(i);

            // Check for a surrogate pair.
            if (0xD800 <= charCode && charCode <= 0xDBFF) {
                var lowCharCode = input.charCodeAt(++i);
                if (isNaN(lowCharCode)) {
                    throw new Error("isNaN(lowCharCode)");
                }
                charCode = ((charCode - 0xD800) << 10) + (lowCharCode - 0xDC00)
                        + 0x10000;
            }

            if (charCode <= 0x7F) {
                this.writeHex(charCode);
            } else if (charCode <= 0x7FF) {
                this.writeHex(charCode >> 6 & 0x1F | 0xC0);
                this.writeHex(charCode & 0x3F | 0x80);
            } else if (charCode <= 0xFFFF) {
                this.writeHex(charCode >> 12 & 0x0F | 0xE0);
                this.writeHex(charCode >> 6 & 0x3F | 0x80);
                this.writeHex(charCode & 0x3F | 0x80);
            } else {
                this.writeHex(charCode >> 18 & 0x07 | 0xF0);
                this.writeHex(charCode >> 12 & 0x3F | 0x80);
                this.writeHex(charCode >> 6 & 0x3F | 0x80);
                this.writeHex(charCode & 0x3F | 0x80);
            }
            ;
        }
        return this.byteStream;
    }

    this.UTF8Length = function(input) {
        var output = 0;
        for (var i = 0; i < input.length; i++) {
            var charCode = input.charCodeAt(i);
            if (charCode > 0x7FF) {
                // Surrogate pair means its a 4 byte character
                if (0xD800 <= charCode && charCode <= 0xDBFF) {
                    i++;
                    output++;
                }
                output += 3;
            } else if (charCode > 0x7F)
                output += 2;
            else
                output++;
        }
        return output;
    }

    this.parseUTF8 = function(input, offset, length) {
        var output = "";
        var utf16;
        var pos = offset;

        while (pos < offset + length) {
            var byte1 = input[pos++];
            if (byte1 < 128)
                utf16 = byte1;
            else {
                var byte2 = input[pos++] - 128;
                if (byte2 < 0)
                    throw new Error("byte2 < 0");
                if (byte1 < 0xE0) // 2 byte character
                    utf16 = 64 * (byte1 - 0xC0) + byte2;
                else {
                    var byte3 = input[pos++] - 128;
                    if (byte3 < 0)
                        throw new Error("byte3 < 0");
                    if (byte1 < 0xF0) // 3 byte character
                        utf16 = 4096 * (byte1 - 0xE0) + 64 * byte2 + byte3;
                    else {
                        var byte4 = input[pos++] - 128;
                        if (byte4 < 0)
                            throw new Error("byte4 < 0");
                        if (byte1 < 0xF8) // 4 byte character
                            utf16 = 262144 * (byte1 - 0xF0) + 4096 * byte2 + 64
                                    * byte3 + byte4;
                        else
                            // longer encodings are not supported
                            throw new Error("longer encodings are not supported");
                    }
                }
            }

            if (utf16 > 0xFFFF) // 4 byte character - express as a surrogate
            // pair
            {
                utf16 -= 0x10000;
                output += String.fromCharCode(0xD800 + (utf16 >> 10)); // lead
                // character
                utf16 = 0xDC00 + (utf16 & 0x3FF); // trail character
            }
            output += String.fromCharCode(utf16);
        }
        return output;
    }

    this.encode = function(bytes) {
        for (var i = 0; i < bytes.length; i++) {
            bytes[i] ^= 0xFF;
        }
        return bytes;
    }
}